package cn.highset.redis.service;

import cn.highset.redis.base.CacheExceptionConstant;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * hash 操作
 *
 * @author gg
 * @version RedisHashService.java, v 0.1 2019-04-09 15:17 gg
 */
public class RedisHashService extends RedisBaseService {


    private RedisTemplate<String, Object> redisTemplate;

    public RedisHashService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }


    /**
     * 对应 命令 hDel
     * 删除哈希表 key 中的一个或多个指定域，不存在的域将被忽略。
     * 在Redis2.4以下的版本里， HDEL 每次只能删除单个域，如果你需要在一个原子时间内删除多个域，请将命令包含在 MULTI / EXEC 块内
     *
     * @param key    must not be {@literal null}.
     * @param fields must not be {@literal null}.
     * @return 被成功移除的域的数量，不包括被忽略的域。
     */
    public Long hDel(final String key, Object... fields) {
        checkKey(key);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.delete(key, fields);
    }

    /**
     * 对应 hExists 命令
     * 检查给定域 field 是否存在于哈希表 hash 当中
     *
     * @param key   must not be {@literal null}.
     * @param field must not be {@literal null}.
     * @return 命令在给定域存在时返回 1 ， 在给定域不存在时返回 0
     */
    public Boolean hExists(final String key, final String field) {
        checkKey(key);
        checkField(field);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.hasKey(key, field);
    }

    /**
     * 对应命令 hGet
     *
     * @param key   must not be {@literal null}.
     * @param field must not be {@literal null}.
     * @return 命令在默认情况下返回给定域的值。
     * 如果给定域不存在于哈希表中， 又或者给定的哈希表并不存在， 那么命令返回 null
     */
    public Object get(final String key, final String field) {
        checkKey(key);
        checkField(field);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.get(key, field);
    }

    /**
     * 对应命令 hMGet
     * <p>
     * 返回哈希表 key 中，一个或多个给定域的值。
     * 如果给定的域不存在于哈希表，那么返回一个 null 值。
     * 因为不存在的 key 被当作一个空哈希表来处理，所以对一个不存在的 key 进行 hMGet 操作将返回一个只带有 null 值的表。
     *
     * @param key    must not be {@literal null}.
     * @param fields must not be {@literal null}.
     * @return 一个包含多个给定域的关联值的表，表值的排列顺序和给定域参数的请求顺序一样。
     */
    public List<Object> hMGet(final String key, Collection<String> fields) {
        checkKey(key);
        if (CollectionUtils.isEmpty(fields)) {
            throw new IllegalArgumentException("fields列表不能为空");
        }
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.multiGet(key, fields);
    }

    /**
     * 对应命令 hIncrBy
     * <p>
     * 为哈希表 key 中的域 field 的值加上增量 increment 。
     * 增量也可以为负数，相当于对给定域进行减法操作。
     * 如果 key 不存在，一个新的哈希表被创建并执行 hIncrBy 命令。
     * 如果域 field 不存在，那么在执行命令前，域的值被初始化为 0 。
     * 对一个储存字符串值的域 field 执行 hIncrBy 命令将造成一个错误。
     * 本操作的值被限制在 64 位(bit)有符号数字表示之内。
     *
     * @param key   must not be {@literal null}.
     * @param field must not be {@literal null}.
     * @param delta 增长的值
     * @return 执行 HINCRBY 命令之后，哈希表 key 中域 field 的值
     */
    public Long hIncrBy(final String key, final String field, long delta) {
        checkKey(key);
        checkField(field);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.increment(key, field, delta);
    }

    /**
     * 对应命令 hIncrByFloat
     * 为哈希表 key 中的域 field 加上浮点数增量 increment 。
     * 如果哈希表中没有域 field ，那么 hIncrByFloat 会先将域 field 的值设为 0 ，然后再执行加法操作。
     * 如果键 key 不存在，那么 hIncrByFloat 会先创建一个哈希表，再创建域 field ，最后再执行加法操作。
     * 当以下任意一个条件发生时，返回一个错误：
     * 域 field 的值不是字符串类型(因为 redis 中的数字和浮点数都以字符串的形式保存，所以它们都属于字符串类型）
     * 域 field 当前的值或给定的增量 increment 不能解释(parse)为双精度浮点数(double precision floating point number)
     *
     * @param key   must not be {@literal null}.
     * @param field must not be {@literal null}.
     * @param delta 增长的值
     * @return 执行加法操作之后 field 域的值
     */
    public Double increment(final String key, final String field, double delta) {
        checkKey(key);
        checkField(field);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.increment(key, field, delta);
    }

    /**
     * 对应命令 hKeys
     * 返回哈希表 key 中的所有field
     *
     * @param key must not be {@literal null}.
     * @return 一个包含哈希表中所有域的表, 当 key 不存在时，返回一个空表
     */
    public Set<String> hKeys(final String key) {
        checkKey(key);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.keys(key);
    }

    /**
     * 对应命令 hLen
     * 返回哈希表 key 中域的数量
     *
     * @param key must not be {@literal null}.
     * @return 哈希表中域的数量，当 key 不存在时，返回 0
     */
    public Long hLen(final String key) {
        checkKey(key);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.size(key);
    }

    /**
     * 对应命令 hMSet
     * 同时将多个 field-value (域-值)对设置到哈希表 key 中
     * 此命令会覆盖哈希表中已存在的域。
     * 如果 key 不存在，一个空哈希表被创建并执行 hMSet 操作
     *
     * @param key must not be {@literal null}.
     * @param map must not be {@literal null}.
     */
    public void hMSet(final String key, Map<String, Object> map) {
        checkKey(key);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        hashOperations.putAll(key, map);
    }

    /**
     * 对应命令 hSet
     * 将哈希表 hash 中域 field 的值设置为 value
     * 如果给定的哈希表并不存在， 那么一个新的哈希表将被创建并执行 hSet 操作
     * 如果域 field 已经存在于哈希表中， 那么它的旧值将被新值 value 覆盖
     *
     * @param key   must not be {@literal null}.
     * @param field must not be {@literal null}.
     * @param value 值
     */
    public void hSet(final String key, final String field, Object value) {
        checkKey(key);
        checkField(field);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        hashOperations.put(key, field, value);
    }

    /**
     * 对应命令 hSetNX
     * 当且仅当域 field 尚未存在于哈希表的情况下， 将它的值设置为 value 。
     * 如果给定域已经存在于哈希表当中， 那么命令将放弃执行设置操作。
     * 如果哈希表 hash 不存在， 那么一个新的哈希表将被创建并执行 hSetNX 命令
     *
     * @param key   must not be {@literal null}.
     * @param field must not be {@literal null}.
     * @param value 值
     * @return 命令在设置成功时返回 1 ， 在给定域已经存在而放弃执行设置操作时返回 0
     */
    public Boolean hSetNX(final String key, final String field, Object value) {
        checkKey(key);
        checkField(field);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.putIfAbsent(key, field, value);
    }

    /**
     * 对应命令 hVals
     * 返回哈希表 key 中所有域的值
     *
     * @param key must not be {@literal null}.
     * @return 一个包含哈希表中所有值的表，当 key 不存在时，返回一个空表。
     */
    public List<Object> values(final String key) {
        checkKey(key);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.values(key);
    }

    /**
     * 对应命令 hGetAll
     * 返回哈希表 key 中，所有的域和值。
     * 在返回值里，紧跟每个域名(field name)之后是域的值(value)，所以返回值的长度是哈希表大小的两倍。
     *
     * @param key must not be {@literal null}.
     * @return 以列表形式返回哈希表的域和域的值 若 key 不存在，返回空列表
     */
    public Map<String, Object> entries(final String key) {
        checkKey(key);
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.entries(key);
    }

    /**
     * 对应  hScan 命令
     * hScan 命令用于迭代哈希键中的键值对
     * 每次执行都只会返回少量元素， 所以这些命令可以用于生产环境， 而不会出现像 KEYS 命令、 sMembers 命令带来的问题
     * 增量式迭代命令只能对被返回的元素提供有限的保证 期间键可能被修改
     *
     * @param key     must not be {@literal null}.
     * @param limit   需设置 两个属性 count 和 pattern count 即为开始搜索的游标
     * @param pattern pattern 即为 key 的 匹配规则
     * @return Cursor 游标 SCAN 命令每次被调用之后， 都会向用户返回一个新的游标，
     * 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数， 以此来延续之前的迭代过程
     * 注意 1.8.0 以后，cursor 不能手动关闭 手动关闭方式:  Closeable.close()
     * @since 1.4
     */
    public Cursor<Map.Entry<String, Object>> hScan(final String key, final String pattern, long limit) {
        checkKey(key);
        checkPattern(pattern);
        ScanOptions options = ScanOptions.scanOptions().match(pattern).count(limit).build();
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.scan(key, options);
    }

    private void checkField(String field) {
        if (StringUtils.isEmpty(field)) {
            throw new IllegalArgumentException(CacheExceptionConstant.FIELD_NOT_NULL);
        }
    }

}